#!/usr/bin/python
#
# Copyright 2010 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


__author__ = 'api.dwh@google.com (Dan Holevoet)'


import logging
import re
from time import strftime
from xml.etree.ElementTree import *
from xml.parsers.expat import ExpatError
from xmlcompat import *
from google.appengine.ext import db


class InputMissingError(Exception):
  def __init__(self):
    self.message = 'Neither a URL nor raw code was provided.'


class Migrator(object):
  """Setup parameters for performing a migration.

  Attributes:
    is_a_gadget: Whether the contents represent a gadget or plaintext (code)
    raw_xml: The raw XML to migrate.
    migrations: The set of migrations to perform.
    include_wrappers: A boolean indicating whether compatibility classes should
      be emitted in the output.
    parsed: The ElementTree representation of the XML.
    content_sections: The set of content sections contained within the XML.
  """

  def __init__(self, raw_code, migrations, include_wrappers=True):
    """Constructor for Migrator.

    If no migrations are specified, the output will match the input.
    """

    self.migrations = migrations
    self.include_wrappers = include_wrappers

    content_sections = []

    # Determine if the raw_code is XML or text
    try:
      # Runs the raw xml through the parser to eliminate reordering in the
      # old/new diff
      header = '<?xml version="1.0" encoding="UTF-8" ?>\n'
      self.raw_code = header + tostring(XML(raw_code))

      self.parsed = XML(raw_code)
      self.raw_content_sections = self.parsed.findall('Content')

      self.is_a_gadget = True

      for raw_content_section in self.raw_content_sections:
        content_sections.append(ContentSection(raw_content_section))
    except ExpatError:
      self.raw_code = raw_code
      self.is_a_gadget = False
      self.parsed = raw_code

      cs_dict = {
          'text': raw_code
      }
      content_sections.append(ContentSection(cs_dict, False))

    self.content_sections = content_sections

    if not raw_code:
      raise InputMissingError()

  def migrate(self):
    """Migrates the parsed XML using the migrations provided during config."""
    for c in self.content_sections:
      c.migrate(self.migrations, self.include_wrappers)

    self.string = self.__str__()

    if self.include_wrappers:
      migrations = filter(lambda m: (m.annotation and not m.wrapper),
          self.migrations)
    else:
      migrations = filter(lambda m: (m.annotation), self.migrations)

    lines = self.string.splitlines()

    annotations = {}
    for migration in migrations:
      if self.string.find(migration.old) >= 0:
        for i, line in enumerate(lines):
          if line.find(migration.old) >= 0:
            index = i + 1 # Displayed line numbers should start at 1
            if index in annotations:
              annotations[index].append(migration.annotation)
            else:
              annotations[index] = [migration.annotation]

    l_annotations = annotations.items()
    l_annotations.sort(lambda x, y: cmp(x[0], y[0]))
    self.annotations = l_annotations

  def __str__(self):
    """Returns the textual form of the migrated content.
    
    If the content represents a gadget, it prepends the XML version header
    which is stripped out via the parsing library (to maintain consistency).
    
    Returns:
      The textual (ie. string) form of the migrated content.
    """
    if self.is_a_gadget:
      header = '<?xml version="1.0" encoding="UTF-8" ?>\n'
      return header + tostring(self.parsed)
    else:
      return self.content_sections[0].raw_section['text']


class Migration(db.Model):
  """Describes a legacy => gadgets.* migration.

  This describes the legacy API call and the mapping to an equivalent gadgets.*
  call, if one is available. If not available, it will provide the code for a
  method that wraps gadgets.* to provide an equivalent. Finally, if that is
  also not available, it will provide an annotation to the appropriate
  documentation containing pointers on how to replace the particular API call.
  """
  old = db.StringProperty()
  new = db.StringProperty()
  regex = db.BooleanProperty()
  wrapper = db.TextProperty()
  annotation = db.TextProperty()


class ContentSection(object):
  """Describes a section of content.

  A gadget may contain multiple content sections (be they <Content> tags, or a
  blocks of text), filled with a mixture of HTML and JS. A non-gadget content
  section is a single block of text. Each ContentSection instance contains the
  migrated text, supplemental text (wrappers), and annotations.
  
  Attributes:
    raw_section: The raw content section of the code to migrate.
    is_a_gadget: A boolean, whether or not the code describes a gadget.
  """

  def __init__(self, raw_section, is_a_gadget=True):
    """Constructor for ContentSection."""
    self.raw_section = raw_section
    self.is_a_gadget = is_a_gadget

  def migrate(self, migrations, include_wrappers=True):
    """Migrates the raw text and adds supplements."""
    supplements = {}

    # The raw section may be one or more CDATA blocks, or a single text
    # block.
    has_children = True
    children = list(self.raw_section)
    if not children or not self.is_a_gadget:
      has_children = False
      children = [self.raw_section]

    for index, child in enumerate(children):
      if self.is_a_gadget:
        text = child.text
      else:
        text = child['text']
      for migration in migrations:
        if not migration.regex:
          if text.find(migration.old) >= 0:
            if migration.new:
              text = text.replace(migration.old, migration.new)
            elif migration.wrapper and include_wrappers:
              supplements[migration.old] = migration.wrapper
        else:
          text = re.sub(migration.old, migration.new, text)
      if has_children:
        self.raw_section[index].text = text
      elif not self.is_a_gadget:
        self.raw_section['text'] = text
      else:
        self.raw_section.text = text

    if supplements:
      if self.is_a_gadget:
        supplement_prefix = '\n<script type="text/javascript">'
        supplement_postfix = '</script>\n'
      else:
        supplement_prefix = ''
        supplement_postfix = ''

      supplement = [supplement_prefix]
      supplement.extend(supplements.values())
      supplement.append(supplement_postfix)

      if self.is_a_gadget:
        supplement_section = CDATA('\n'.join(supplement))
        self.raw_section.insert(0, supplement_section)
      else:
        self.raw_section['text'] = '\n'.join(supplement) + '\n' + \
            self.raw_section['text']
